﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Chat.Server
{
    /// <summary>
    /// Args for event risen by the chat server, when new user logged in/out
    /// </summary>
    public class UserConnectEventArgs : EventArgs
    {
        /// <summary>
        /// The nick name of the user who logged in/out.
        /// </summary>
        public NickName User { get; protected set; }
        /// <summary>
        /// Whether it was login or logout.
        /// </summary>
        public UserConnectState ConnectState { get; protected set; }

        public UserConnectEventArgs(NickName user, UserConnectState state)
        {
            User = user;
            ConnectState = state;
        }
    }
    /// <summary>
    /// Args for event risen by the chat server, when new users logged in/out.
    /// </summary>
    public class UsersConnectEventArgs : EventArgs
    {
        /// <summary>
        /// The nick names of the users that logged in/out
        /// </summary>
        public ICollection<NickName> Users { get; protected set; }
        /// <summary>
        /// Whether it was login or logout.
        /// </summary>
        public UserConnectState ConnectState { get; protected set; }

        public UsersConnectEventArgs(ICollection<NickName> users, UserConnectState state)
        {
            Users = users;
            ConnectState = state;
        }
    }

    /// <summary>
    /// The chat server core.
    /// Holds a collection of ServerEndPoint instances, where each instanse
    /// is used to communicate with a particular chat client, currently connected
    /// to the server. Is responsible for creating new instanses of ServerEndPoint.
    /// Has public events, risen when new client connects or existing client
    /// disconnects.
    /// MUST SEE the BaseEndPoint class documentation before.
    /// </summary>
    public class Server
    {
        /// <summary>
        /// The listener for incomming chat clients connections.
        /// </summary>
        private WelcomeClient welcomeClient;

        /// <summary>
        /// Collection of chat clients, currently connected to the server.
        /// Each ServerEndPoint instanse for one particular client.
        /// </summary>
        private ServerEndPointCollection endPoints = new ServerEndPointCollection();

        /// <summary>
        /// The mutex of the collection of chat clients.
        /// </summary>
        private ReaderWriterLockSlim endPointsLock = new ReaderWriterLockSlim();

        /// <summary>
        /// Initializes a new instanse of the Server with specified IPEndPoint
        /// to listen for new incoming clients connections on.
        /// </summary>
        /// <param name="listenOn">
        /// The IPEndPoint to listen for incoming client connections.
        /// </param>
        public Server(IPEndPoint listenOn)
        {
            welcomeClient = new WelcomeTcpClient(listenOn);

            Initialize();
        }

        /// <summary>
        /// Occurs, when a single client loggs in/out.
        /// </summary>
        public event EventHandler<UserConnectEventArgs> UserConnection;
        /// <summary>
        /// Occurs, when a group of clients log in/out.
        /// </summary>
        public event EventHandler<UsersConnectEventArgs> UsersConnection;
        /// <summary>
        /// Occurs, when server fails and can't be uses any more.
        /// </summary>
        public event EventHandler ServerFailed;
        /// <summary>
        /// Starts the server. Actually will start the background thread that
        /// will listen for new incomming clients connections.
        /// </summary>
        public void Start()
        {
            welcomeClient.Start();
        }

        /// <summary>
        /// Stops the server. Actually will stop the background thread that
        /// will listen for new incomming clients connections.
        /// </summary>
        public void Stop()
        {
            try
            {
                welcomeClient.Stop();
            }
            finally
            {
                DisposeEndpoints();
            }
        }

        /// <summary>
        /// Disposes the specified ServerEndPoint, including closing the underlying
        /// connection with the client, and removes the specified ServerEndPoint
        /// from the collection.
        /// After the disposion and removal, the remaining clients(each represented
        /// by the ServerEndPoint instanse in the collection) are notified about
        /// the client removal.
        /// </summary>
        /// <param name="sep">The end point to dispose and remove.</param>
        protected virtual void RemoveEndPoint(ServerEndPoint sep)
        {
            NickName removedUser = sep.LoginData.User;

            endPointsLock.EnterUpgradeableReadLock();
            try
            {
                //
                // Aparently already removed. This can happen, when e.g. the end point
                // fails simultaneously on Send and Receive operations of
                // the chat command.
                if (!endPoints.Contains(sep))
                    return;

                //
                // rise event about the removed client.
                OnUserConnection(new UserConnectEventArgs(
                    removedUser, UserConnectState.Disconnected));

                //
                // Remove the ServerEndPoint from the collection.
                endPointsLock.EnterWriteLock();
                endPoints.Remove(sep);
                endPointsLock.ExitWriteLock();

                //
                // Notify the remaining chat clients about the user disconnect.
                endPoints.ForEach(
                    ep => ep.SendUserUpdate(removedUser, UserConnectState.Disconnected),
                    null);
            }
            finally
            {
                endPointsLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Functionally does the same as RemoveEndPoint does, but for multiple
        /// ServerEndPoint instanses. Also, it's assumed, that the endpoints specified
        /// for removal, are failed end points, i.e. endpoints, that stop functioning.
        /// </summary>
        /// <param name="seps">
        /// The failed endpoints to remove. The Exception (value from the pair)
        /// holds the reson, caused the endpoint to fail.
        /// </param>
        protected virtual void RemoveFailedEndPoints(
            ICollection<KeyValuePair<ServerEndPoint, Exception>> seps)
        {
            LinkedList<NickName> removedUsers = new LinkedList<NickName>();

            endPointsLock.EnterUpgradeableReadLock();
            try
            {
                //
                // Try to remove the endpoints from the collection first.
                endPointsLock.EnterWriteLock();
                foreach (KeyValuePair<ServerEndPoint, Exception> sep in seps)
                {
                    if (endPoints.Remove(sep.Key))
                        removedUsers.AddLast(sep.Key.LoginData.User);
                }
                endPointsLock.ExitWriteLock();

                if (removedUsers.Count == 0)
                    return;

                //
                // Notify the remianing clients, about all successfully removed
                // endpoints above.
                endPoints.ForEach(
                    sep => sep.SendUsersUpdate(removedUsers, UserConnectState.Disconnected),
                    null);

                //
                // rise event about the multiple users disconnected.
                OnUsersConnection(new UsersConnectEventArgs(
                    removedUsers, UserConnectState.Disconnected));
            }
            finally
            {
                endPointsLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Rises UserConnection event.
        /// </summary>
        /// <param name="e">The UserConnection event args.</param>
        protected virtual void OnUserConnection(UserConnectEventArgs e)
        {
            if (UserConnection != null)
                UserConnection(this, e);
        }

        /// <summary>
        /// Rises UsersConnection event.
        /// </summary>
        /// <param name="e">The UsersConnection event args.</param>
        protected virtual void OnUsersConnection(UsersConnectEventArgs e)
        {
            if (UsersConnection != null)
                UsersConnection(this, e);
        }

        /// <summary>
        /// Rises ServerFailed event.
        /// </summary>
        protected virtual void OnServerFailed()
        {
            if (ServerFailed != null)
                ServerFailed(this, EventArgs.Empty);
        }

        /// <summary>
        /// Initializes event handlers of the welcomeClient listener (the background
        /// thread, who listens for new clients incomming connections).
        /// Is called from the ctor only!!!
        /// </summary>
        private void Initialize()
        {
            welcomeClient.ClientConnected +=
                new EventHandler<ClientConnectedEventArgs>(
                    welcomeClient_ClientConnected);

            welcomeClient.Exit += new EventHandler(welcomeClient_Exit);
        }

        /// <summary>
        /// Disposes each ServerEndPoint in the collection, and clear the collection
        /// itself.
        /// </summary>
        private void DisposeEndpoints()
        {
            endPointsLock.EnterWriteLock();
            endPoints.Clear(sep => sep.Dispose());
            endPointsLock.ExitWriteLock();
        }

        /// <summary>
        /// Event handler for the Exit event of the welcomeClient listener
        /// (the background thread, who listens for new clients incomming connections).
        /// Since, there is no sense to keep working without the listener,
        /// all endpoints are disposed and ServerFailed event risen, indicating the
        /// server failure.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void welcomeClient_Exit(object sender, EventArgs e)
        {
            DisposeEndpoints();

            OnServerFailed();
        }

        /// <summary>
        /// Event handler for the ClientConnected of the welcomeClient listener
        /// (the background thread, who listens for new clients incomming connections).
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void welcomeClient_ClientConnected(object sender, ClientConnectedEventArgs e)
        {
            ServerEndPoint sep = CreateNewEndPoint(e.Connection);

            if (sep == null)
                return;

            //
            // If the endpoint successfully created, response to the underlying
            // chat client with the LoginOk response.
            try
            {
                sep.SendLoginOk();
            }
            catch (Exception)
            {
                return;
            }

            //
            // Set handlers for the endpoint events.
            sep.MessageReceived += new EventHandler<MessageEventArgs>(
                ServerEndPoint_MessageReceived);
            sep.Logout += new EventHandler<LogoutEventArgs>(
                ServerEndPoint_Logout);
            sep.AbnormallyTerminated += new EventHandler<EndPointAbnormallyTerminatedEventArgs>(
                ServerEndPoint_AbnormallyTerminated);

            NickName newClientNick = sep.LoginData.User;

            //
            // Now we lock the collection. This will guarantee, that no endpoint
            // will be removed/added from the collection before the list of currently
            // logged in users (sep.SendUsersOnline bellow) sent to the new chat
            // client.
            endPointsLock.EnterUpgradeableReadLock();
            try
            {
                sep.SendUsersOnline(endPoints.NickNames);

                //
                // start the background listener for client commands.
                BaseEndPoint<ServerCmd>.Start(sep);

                //
                // Notify other chat users about the new user.
                endPoints.ForEach(
                    ep => ep.SendUserUpdate(newClientNick, UserConnectState.Connected),
                    null);

                //
                // Rise UserConnection event of the Server.
                OnUserConnection(new UserConnectEventArgs(
                    newClientNick, UserConnectState.Connected));

                //
                // And the endpoint to the collection.
                endPointsLock.EnterWriteLock();
                endPoints.Add(sep);
                endPointsLock.ExitWriteLock();
            }
            catch (Exception)
            {
                return;
            }
            finally
            {
                endPointsLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Event handler for the AbnormallyTerminated event risen by
        /// ServerEndPoint instanse.
        /// Will dispose the abnormally terminated endpoint and
        /// remove it from the collection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ServerEndPoint_AbnormallyTerminated(object sender, EndPointAbnormallyTerminatedEventArgs e)
        {
            ServerEndPoint sep = sender as ServerEndPoint;
            RemoveEndPoint(sep);
        }

        /// <summary>
        /// Event handler for the Logout event risen by ServerEndPoint instanse.
        /// Will dispose the abnormally terminated endpoint and
        /// remove it from the collection.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ServerEndPoint_Logout(object sender, LogoutEventArgs e)
        {
            ServerEndPoint sep = sender as ServerEndPoint;
            RemoveEndPoint(sep);
        }

        /// <summary>
        /// Event handler for the MessageReceived event risen by ServerEndPoint instanse.
        /// Resends the chat message to the endpoints (chat clients) to whom the message
        /// was sent.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ServerEndPoint_MessageReceived(object sender, MessageEventArgs e)
        {
            ServerEndPoint senderSep = sender as ServerEndPoint;
            NickName toNn = e.CommandReceived.MessageData.To;
            string message = e.CommandReceived.MessageData.Message;
            LinkedList<KeyValuePair<ServerEndPoint, Exception>> failedSeps =
                new LinkedList<KeyValuePair<ServerEndPoint, Exception>>();

            //
            // The private message case. The message is only sent to the private
            // receipient and the sender himself.
            if (toNn != NickName.Broadcast)
            {
                ServerEndPoint toSep;

                endPointsLock.EnterReadLock();
                try
                {
                    //
                    // Return, if the receipient not exists any more.
                    // Issue: should we send the message to the sender?
                    if (!endPoints.TryGetEndPoint(toNn, out toSep))
                        return;
                }
                finally
                {
                    endPointsLock.ExitReadLock();
                }

                ServerEndPointCollection tmpEndPoints = new ServerEndPointCollection();

                tmpEndPoints.Add(toSep);
                tmpEndPoints.Add(senderSep);

                //
                // See the comment bellow (in the else block) about why we need
                // to use SendMessageWithLock method here.
                tmpEndPoints.ForEach(
                    sep => sep.SendMessageWithLock(toNn, message, senderSep.LoginData),
                    failedSeps);

                tmpEndPoints.Clear();
            }
            //
            // The broadcast case. The message is sent to everybody, including the sender.
            else
            {
                endPointsLock.EnterReadLock();
                // This event handler is executed in the context of the
                // endpoint's background thread, responsible for receiving chat
                // commands (see CmdReceiverThread data member of the BaseEndPoint class).
                // There may be several endpoints (chat clients) at the same time,
                // each willing to send a message to the same endpoint (chat client).
                // Thus Sending the message must be done in critical section, which is
                // provided by the SendMessageWithLock method. Otherwise, the data
                // sent by one endpoint, can mix with data sent by another endpoint
                // to the same chat client.
                // It would be possible just to use write lock on the collection,
                // when sending a message, but this would be less effective.
                endPoints.ForEach(
                    sep => sep.SendMessageWithLock(toNn, message, senderSep.LoginData),
                    failedSeps);
                endPointsLock.ExitReadLock();
            }

            if (failedSeps.Count == 0)
                return;

            //
            // Remove all the endpoints,
            // failed on SendMessageWithLock() method invoked on them.
            RemoveFailedEndPoints(failedSeps);
        }

        /// <summary>
        /// Creates new ServerEndPoint instanse for the specified connection.
        /// Checks, whether the NickName of the endpoint is not in use.
        /// Will dispose the created enpoint, if its NichName in use.
        /// </summary>
        /// <param name="epConnection"></param>
        /// <returns></returns>
        private ServerEndPoint CreateNewEndPoint(
            Connection<ServerCmd> epConnection)
        {
            ServerEndPoint sep = null;

            CmdSerializer<ServerCmd> serializer =
                ConnectionFactory<ServerCmd>.CreateSerializer(epConnection);
            try
            {
                //
                // The endpoint's ctor will accept the client's Login.
                sep = ServerEndPoint.Create(epConnection, serializer);
            }
            catch (Exception)
            {
                return null;
            }

            //
            // Check if the endpoint's nickname, accepted in its constructor,
            // is already in use.
            endPointsLock.EnterReadLock();
            bool userExists = endPoints.Contains(sep);
            endPointsLock.ExitReadLock();

            if (userExists)
            {
                try
                {
                    sep.SendLoginFailed(string.Format(
                        "User {0} already logged in", sep.LoginData.User));

                    sep.Dispose();
                }
                catch (Exception)
                {
                    // no need to dispose, will be disposed automatically upon
                    // exception occurred in SendLoginFailed method
                }

                return null;
            }

            return sep;
        }

    }
}
